package org.hawk.gwt.ppc.reflect;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.hawk.gwt.ppc.Configuration;
import org.hawk.gwt.ppc.GWTModule;
import org.hawk.gwt.ppc.Log;
import org.hawk.gwt.ppc.ProcessingUnitsManager;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.JavaTypeScope;
import org.hawk.gwt.ppc.introspection.SourcesRepository;
import org.hawk.gwt.ppc.loader.ProcessingUnit;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.JavaKeywords;
import org.hawk.gwt.ppc.parser.Token;
import org.hawk.gwt.ppc.reflect.lang.ClassDelegate;
import org.hawk.gwt.ppc.reflect.lang.ClassLoaderDelegate;
import org.hawk.gwt.ppc.reflect.lang.LangClassLoader;
import org.hawk.gwt.ppc.reflect.lang.SystemClassLoader;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.Pattern;
import org.hawk.gwt.ppc.utils.PreprocessorException;
import org.hawk.gwt.ppc.utils.StringUtils;
import org.hawk.gwt.ppc.utils.UniqueList;

import com.google.gwt.dev.util.collect.HashSet;

/**
 * Collects all the metadata for classes and links it to the entry point of
 * the GWT module. If module has more than one entry point - all classes will be 
 * linked to the first one.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public final class MetadataProcessor implements ProcessingUnit {
	private static final String JAVA_LANG_CLASS_PREFIX = "java.lang.";
	private static final String CLASS_PROPERTY = "class";
	private static final String CLASS_DELEGATE_PROPERTY = "$class";
	
	private List<Pattern> pattern;
	private SourcesRepository repository;
	private Configuration configuration;
	
	private List<String> interfaces;
	private Set<String> thirdPartyClasses;
	private StringBuilder output;
	private List<String> processedClasses;
	
	/**
	 * Creates metadata processor.
	 */
	public MetadataProcessor(){
		reinit();
	}
	
	private void reinit() {
		interfaces = new UniqueList<String>();
		thirdPartyClasses = new HashSet<String>();
		output = new StringBuilder();
		processedClasses = new LinkedList<String>();
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#configure(org.hawk.gwt.ppc.Configuration, org.hawk.gwt.ppc.ProcessingUnitsManager)
	 */
	public void configure(Configuration config, ProcessingUnitsManager manager) {
		repository = manager.getRepository();
		configuration = config;
		pattern = new LinkedList<Pattern>();
		
		String fullPattern = config.get(Configuration.REFLECTION_PARAMETER);
		if (fullPattern == null || fullPattern.trim().length() == 0) {
			return;
		}
		
		for (String p : fullPattern.split(",")) {
			pattern.add(config.getNewPattern(p));
		}
		pattern = Collections.unmodifiableList(pattern);
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#processModule(org.hawk.gwt.ppc.GWTModule)
	 */
	public void processModule(GWTModule module) throws PreprocessorException {
		module.addInheritance("org.hawk.gwt.ppc.reflect.lang");
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#processEntryPoint(java.lang.String)
	 */
	public void processEntryPoint(String entryPoint)
			throws PreprocessorException {
		if (Log.isDebugEnabled()) {
			logReflectionClassesForThisModule(entryPoint);
		}
		CompilationUnitDescriptor compilationUnit = repository.findCompilationUnit(entryPoint);
		if (compilationUnit.getTopClasses().isEmpty()) {
			return;
		}
		ClassDescriptor point = compilationUnit.getTopClasses().get(0);
		point.getLastToken().getRaw().insert(0, createClassLoaderDefinition());
		repository.markChanged(point.getName());
		reinit();
	}

	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#acceptResource(java.lang.String)
	 */
	public boolean acceptResource(String resource) {
		for (Pattern p : pattern) {
			if (p.match(resource)) {
				return true;
			}
		}
		return false;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#processCompilationUnit(java.lang.String)
	 */
	public void processCompilationUnit(String compilationUnitName)
			throws PreprocessorException {
		CompilationUnitDescriptor compilationUnit = repository.findCompilationUnit(compilationUnitName);
		
		for (ClassDescriptor clazz : compilationUnit.getTopClasses()) {
			buildClass(clazz);
		}
	}

	private void buildClass(ClassDescriptor clazz) throws PreprocessorException {
		if (processedClasses.contains(clazz.getName())) {
			return;
		}
		if (!clazz.getModifiers().contains(JavaKeywords.PUBLIC)) {
			clazz.getFirstToken().getRaw().insert(0, "public ");
		}
		repository.markChanged(clazz.getName());
		JavaTypeScope type = IntrospectionUtils.getType(clazz);
		
		if (type.isAnnotation() && !configuration.get(Configuration.PROCESS_ANNOTATIONS_PARAMETER, Boolean.FALSE)) {
			return; // skip annotation if not processed explicitly
		}
		
		ClassDelegateBuilder builder = ClassDelegateBuilder.build(type);
		builder.setInterfaces(collectInterfacesIndices(type, new LinkedList<Integer>()));
		
		if (builder.shouldBuildToClassSource()) {
			StringBuilder result = new StringBuilder(100);
			builder.write(result);
			thirdPartyClasses.addAll(builder.getThirdPartyClasses());
			Token last = clazz.getLastToken();
			last.getRaw().insert(0, result);
			processedClasses.add(clazz.getName());
		} else {
			if (output.length() > 0) {
				output.append(',');
			}
			builder.write(output);
		}
	}
	
	private List<Integer> collectInterfacesIndices(JavaTypeScope type, List<Integer> result) throws PreprocessorException {
		if (type == null) {
			return result;
		}
		for (JavaTypeScope iface : type.getInterfaces()) {
			String name = iface.getName();
			interfaces.add(name);
			result.add(interfaces.indexOf(name));

			collectInterfacesIndices(iface, result);
		}
		collectInterfacesIndices(type.getSuperClass(), result);
		return result;
	}
	


	private String createClassLoaderDefinition() throws PreprocessorException {
		final String ClassDelegate = ClassDelegate.class.getName();
		return "static {\n" + ClassDelegate + ".registerClassLoader(\n"
				+ "new " + ClassLoaderDelegate.class.getName() + "(){\n"
				+ "protected " + ClassDelegate + "<?>[] getClasses(){\n"
				+ "return new " + ClassDelegate
				+ "[]{\n"
				+ getCollectedClassesAsArrayDefinition()
				+ "};\n}\n"
				+ // end of getClasses() method
				"protected String getPackage(){\n" + "return \""
				+ getCommonPackage() + "\";"
				+ "}\n"
				+ // end of getPackage() method
				"protected Class<?>[] getNotReflectableClasses(){\n"
				+ "return new Class[]{\n"
				+ getNotReflectableClassesAsArrayDefinition() + "};\n}\n" + // end of
																		// getNotReflectableClasses()
																		// method
				"protected Class<?>[] getInterfaces(){\n"
				+ "return new Class[]{\n"
				+ getInterfacesAsArrayDefinition() + "};\n}\n" + // end of
																		// getNotReflectableClasses()
																		// method
				"});\n" + // end of ClassLoaderDelegate inner class
				"}\n";// end of static
	}

	private String getCollectedClassesAsArrayDefinition() {
		return toArrayDefinition(processedClasses,
				output.toString(),
				CLASS_DELEGATE_PROPERTY);
	}

	private String getNotReflectableClassesAsArrayDefinition() throws PreprocessorException {
		Set<String> result = new HashSet<String>();
		result.addAll(thirdPartyClasses);
		for (String className : processedClasses) {
			JavaTypeScope scope = IntrospectionUtils.getType(repository.requireClass(className));
			result.addAll(scope.getDependingTypes());
		}
		result.removeAll(processedClasses);
		return toArrayDefinition(result, "", CLASS_PROPERTY);
	}

	private String getInterfacesAsArrayDefinition() {
		Iterator<String> iterator = interfaces.iterator();
		if (!iterator.hasNext()) {
			return "";
		}
		StringBuilder result = new StringBuilder();
		result.append(iterator.next());
		result.append('.');
		result.append(CLASS_PROPERTY);
		
		while (iterator.hasNext()) {
			result.append(',');
			result.append(iterator.next());
			result.append('.');
			result.append(CLASS_PROPERTY);
		}
		return result.toString();
	}

	private String toArrayDefinition(Collection<String> classes, String metadata, String property) {
		Iterator<String> iterator = classes.iterator();
		if (!iterator.hasNext()) {
			return metadata;
		}
		StringBuilder result = new StringBuilder();
		appendArrayItem(property, iterator, result);
		while (iterator.hasNext()) {
			appendArrayItem(property, iterator, result);
		}
		if (result.length() > 0) {
			result.deleteCharAt(0);
		}
		if (metadata.isEmpty()) {
			return result.toString();
		}
		if (result.length() > 0) {
			result.append(',');
		}
		result.append(metadata);
		return result.toString();
	}

	private void appendArrayItem(String property, Iterator<String> iterator,
			StringBuilder result) {
		String typeName = iterator.next();
		if ((typeName.startsWith(JAVA_LANG_CLASS_PREFIX) && LangClassLoader.isLangClass(typeName.substring(JAVA_LANG_CLASS_PREFIX.length()))) 
				|| SystemClassLoader.isPrimitiveClass(typeName) || isNonPublicInternalClass(typeName)) {
			return;
		}
		result.append(',');
		result.append(typeName);
		result.append('.');
		result.append(property);
	}

	private boolean isNonPublicInternalClass(String name) {
		while (Name.isFullName(name)) {
			try {
				Class<?> cls = Class.forName(name, false, getClass().getClassLoader());
				return (cls.isLocalClass() || cls.isAnonymousClass() || cls.isMemberClass()) && !Modifier.isPublic(cls.getModifiers());
			} catch (ClassNotFoundException ex) {
				name = StringUtils.replaceLast(name, ".", "$");
			}
		}
		return false;
	}

	private String getCommonPackage() {
		Iterator<String> iterator = processedClasses.iterator();
		if (!iterator.hasNext())
			return "";
		String packageName = Name.getParentJavaName(iterator.next());
		while (iterator.hasNext()) {
			packageName = Name.getCommonPackage(iterator.next(), packageName);
		}
		return packageName;
	}

	private void logReflectionClassesForThisModule(String entryPoint) {
		Log.debug("\tLinking classes to entry point: " + entryPoint);
		for (String cls : processedClasses) {
			Log.debug("\t\tLinking " + cls);
		}
	}
}
